/*
 * MIT License
 *
 * Copyright (c) 2024-2048 冰羽
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.star.framework.extend.core;

import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.star.framework.extend.config.AvailableSettings;
import cn.star.framework.extend.config.Cfg;
import cn.star.framework.extend.mapping.TableInfo;
import cn.star.framework.extend.schema.extract.internal.NativeInfoExtractorImpl;
import cn.star.framework.extend.schema.internal.SchemaUpdater;
import cn.star.framework.extend.schema.spi.JpaTargetAndSourceDescriptorImpl;
import cn.star.framework.extend.type.ExporterType;
import cn.star.framework.extend.type.TableType;
import java.io.Reader;
import java.io.StringReader;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.relational.Exportable;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.Namespace.Name;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.boot.model.relational.internal.SqlStringGenerationContextImpl;
import org.hibernate.boot.registry.selector.spi.StrategySelector;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.resource.transaction.spi.DdlTransactionIsolator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.tool.schema.SourceType;
import org.hibernate.tool.schema.TargetType;
import org.hibernate.tool.schema.extract.spi.DatabaseInformation;
import org.hibernate.tool.schema.extract.spi.ExtractionContext;
import org.hibernate.tool.schema.extract.spi.ExtractionContext.DatabaseObjectAccess;
import org.hibernate.tool.schema.extract.spi.TableInformation;
import org.hibernate.tool.schema.internal.DefaultSchemaFilterProvider;
import org.hibernate.tool.schema.internal.ExceptionHandlerHaltImpl;
import org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl;
import org.hibernate.tool.schema.internal.Helper;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.internal.SchemaCreatorImpl;
import org.hibernate.tool.schema.internal.SchemaDropperImpl;
import org.hibernate.tool.schema.internal.StandardTableExporter;
import org.hibernate.tool.schema.internal.exec.ScriptSourceInputFromReader;
import org.hibernate.tool.schema.internal.exec.ScriptTargetOutputToStdout;
import org.hibernate.tool.schema.spi.ExecutionOptions;
import org.hibernate.tool.schema.spi.JpaTargetAndSourceDescriptor;
import org.hibernate.tool.schema.spi.SchemaCreator;
import org.hibernate.tool.schema.spi.SchemaDropper;
import org.hibernate.tool.schema.spi.SchemaFilterProvider;
import org.hibernate.tool.schema.spi.SchemaManagementTool;
import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator;
import org.hibernate.tool.schema.spi.ScriptSourceInput;
import org.hibernate.tool.schema.spi.ScriptTargetOutput;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

/**
 * Database<br>
 *
 * <p>调用时加锁，例如
 *
 * <pre>
 *   private static final Map<String, Database> cache = new ConcurrentHashMap<>();
 *   private static final Lock lock = new ReentrantLock();
 *   public static Database get(Cfg cfg) {
 *     try {
 *       String key = cfg; // 计算Cfg以获取唯一的Key
 *       Database database = cache.get(key);
 *       if (database == null) {
 *         lock.lock();
 *         database = cache.get(key);
 *         if (database == null) {
 *           database = new Database(cfg);
 *           cache.put(key, database);
 *         }
 *       }
 *       return database;
 *     } finally {
 *       lock.unlock();
 *     }
 *   }
 * </pre>
 *
 * @author zhaoweiping
 *     <p style='color: red'>Created on 2024-09-23 11:06:00
 * @since 3.0.0
 */
@Slf4j
public final class Database {

  @Getter private final Cfg cfg;

  @Getter private final Dialect dialect;
  @Getter private final Metadata metadata;
  @Getter private final DataSource dataSource;
  @Getter private final SessionFactory sessionFactory;
  @Getter private final PlatformTransactionManager transactionManager;
  @Getter private final ServiceRegistry serviceRegistry;
  @Getter private final JdbcEnvironment jdbcEnvironment;
  @Getter private final org.hibernate.boot.model.relational.Database database;
  @Getter private final ConfigurationService configurationService;

  @Getter private final HibernateSchemaManagementTool tool;
  @Getter private final SchemaCreator creator;
  @Getter private final SchemaDropper dropper;
  @Getter private final SchemaUpdater updater;

  private NativeInfoExtractorImpl getExtractorService() {
    // 连接关闭的问题
    ExtractionContextTuple tuple =
        this.getExtractionContextTuple(
            configurationService, serviceRegistry, tool, jdbcEnvironment, database);

    return new NativeInfoExtractorImpl(tuple);
  }

  private void metaConnectionClose(NativeInfoExtractorImpl extractor) {
    try {
      ExtractionContextTuple tuple = extractor.getExtractionContextTuple();
      ExtractionContext extractionContext = tuple.getExtractionContext();
      DdlTransactionIsolator ddlTransactionIsolator = tuple.getDdlTransactionIsolator();
      ddlTransactionIsolator.release();
      extractionContext.cleanup();
    } catch (Exception e) {
      log.error("close database connection error. ", e);
    }
  }

  Database(Cfg cfg) {
    try {
      this.cfg = cfg;
      LocalSessionFactoryBean bean = new LocalSessionFactoryBean();
      // 不手动设置数据源，使用#setHibernateProperties中读取到cfg中的ConnectionProvider初始化数据源
      bean.setHibernateProperties(cfg.unwrap());
      bean.afterPropertiesSet();

      SessionFactory factory = bean.getObject();

      if (factory == null) {
        throw new RuntimeException(StrUtil.format("{} init fail", SessionFactory.class.getName()));
      }

      this.sessionFactory = factory;
      this.transactionManager = new HibernateTransactionManager(factory);
      this.serviceRegistry = factory.getSessionFactoryOptions().getServiceRegistry();
      this.dataSource =
          serviceRegistry.getService(ConnectionProvider.class).unwrap(DataSource.class);
      this.metadata = new MetadataSources(serviceRegistry).buildMetadata();
      this.database = metadata.getDatabase();
      this.jdbcEnvironment = database.getJdbcEnvironment();
      this.dialect = database.getDialect();
      this.tool =
          (HibernateSchemaManagementTool) serviceRegistry.getService(SchemaManagementTool.class);
      this.configurationService = serviceRegistry.getService(ConfigurationService.class);

      // build ddl
      this.creator = new SchemaCreatorImpl(tool);
      this.dropper = new SchemaDropperImpl(tool);
      StrategySelector selector = serviceRegistry.getService(StrategySelector.class);
      Map options = configurationService.getSettings();
      final Object strategyReference =
          (options == null) ? null : options.get(Environment.HBM2DDL_FILTER_PROVIDER);
      SchemaFilterProvider provider =
          selector.resolveDefaultableStrategy(
              SchemaFilterProvider.class, strategyReference, DefaultSchemaFilterProvider.INSTANCE);
      this.updater = new SchemaUpdater(tool, provider.getMigrateFilter());

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /** 获取{@link Session} */
  public Session getSession() {
    return this.sessionFactory.openSession();
  }

  /** 释放资源 */
  public void close() {
    this.sessionFactory.close();
  }

  private ExtractionContextTuple getExtractionContextTuple(
      ConfigurationService config,
      ServiceRegistry registry,
      HibernateSchemaManagementTool tool,
      JdbcEnvironment environment,
      org.hibernate.boot.model.relational.Database database) {
    final Map map = configurationService.getSettings();
    final DdlTransactionIsolator ddlTransactionIsolator =
        tool.getDdlTransactionIsolator(tool.resolveJdbcContext(config.getSettings()));
    final SqlStringGenerationContext sqlStringGenerationContext =
        SqlStringGenerationContextImpl.fromConfigurationMap(environment, database, map);
    final DatabaseInformation databaseInformation =
        Helper.buildDatabaseInformation(
            registry, ddlTransactionIsolator, sqlStringGenerationContext, tool);

    ExtractionContext extractionContext =
        tool.getExtractionTool()
            .createExtractionContext(
                registry,
                environment,
                sqlStringGenerationContext,
                ddlTransactionIsolator,
                (DatabaseObjectAccess) databaseInformation);

    return new ExtractionContextTuple(extractionContext, ddlTransactionIsolator);
  }

  private SqlStringGenerationContext getSqlStringGenerationContext() {
    final Map map = configurationService.getSettings();
    return SqlStringGenerationContextImpl.fromConfigurationMap(jdbcEnvironment, database, map);
  }

  private ExecutionOptions getExecutionOptions() {
    final Map map = configurationService.getSettings();
    boolean haltOnError =
        configurationService.getSetting(
            AvailableSettings.HBM2DDL_HALT_ON_ERROR, StandardConverters.BOOLEAN, false);

    return SchemaManagementToolCoordinator.buildExecutionOptions(
        map, haltOnError ? ExceptionHandlerHaltImpl.INSTANCE : ExceptionHandlerLoggedImpl.INSTANCE);
  }

  private Name getDefaultNamespaceName() {
    Namespace namespace = database.getDefaultNamespace();
    return namespace == null ? null : namespace.getName();
  }

  /** 获取当前Catalog */
  public String getDefaultCatalog() {
    Name name = this.getDefaultNamespaceName();
    Identifier catalog = name != null ? name.getCatalog() : null;
    if (catalog == null) {
      catalog = jdbcEnvironment.getCurrentCatalog();
    }

    return catalog == null ? cfg.getCatalog() : NativeInfoExtractorImpl.wrapper(catalog);
  }

  /** 获取当前Schema */
  public String getDefaultSchema() {
    Name name = this.getDefaultNamespaceName();
    Identifier schema = name != null ? name.getSchema() : null;
    if (schema == null) {
      schema = jdbcEnvironment.getCurrentSchema();
    }

    return schema == null ? cfg.getSchema() : NativeInfoExtractorImpl.wrapper(schema);
  }

  /**
   * 获取表名
   *
   * @param types 类型
   * @return 表名列表
   */
  public List<String> getNativeTableNames(TableType... types) {
    return this.getNativeTableNames(this.getDefaultCatalog(), this.getDefaultSchema(), types);
  }

  /**
   * 获取表名
   *
   * @param catalog catalog
   * @param schema schema
   * @param types 类型
   * @return 表名列表
   */
  public List<String> getNativeTableNames(String catalog, String schema, TableType... types) {
    NativeInfoExtractorImpl extractor = this.getExtractorService();
    List<String> tableNames = extractor.getNativeTableNames(catalog, schema, types);
    this.metaConnectionClose(extractor);

    return tableNames;
  }

  /**
   * 获取表名
   *
   * @param catalog catalog
   * @param schema schema
   * @param types 类型
   * @return 表名列表
   */
  public List<String> getNativeTableNames(
      Identifier catalog, Identifier schema, TableType... types) {
    return this.getNativeTableNames(
        NativeInfoExtractorImpl.wrapper(catalog), NativeInfoExtractorImpl.wrapper(schema), types);
  }

  /**
   * 获取表结构
   *
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public Table getTableInfo(String tableName, TableType... types) {
    return this.getTableInfo(this.getDefaultCatalog(), this.getDefaultSchema(), tableName, types);
  }

  /**
   * 表是否存在
   *
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public boolean isTableExists(String tableName, TableType... types) {
    return this.isTableExists(this.getDefaultCatalog(), this.getDefaultSchema(), tableName, types);
  }

  /**
   * 获取表结构
   *
   * @param catalog catalog
   * @param schema schema
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public Table getTableInfo(
      Identifier catalog, Identifier schema, Identifier tableName, TableType... types) {
    return this.getTableInfo(
        NativeInfoExtractorImpl.wrapper(catalog),
        NativeInfoExtractorImpl.wrapper(schema),
        NativeInfoExtractorImpl.wrapper(tableName),
        types);
  }

  /**
   * 表是否存在
   *
   * @param catalog catalog
   * @param schema schema
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public boolean isTableExists(
      Identifier catalog, Identifier schema, Identifier tableName, TableType... types) {
    return this.isTableExists(
        NativeInfoExtractorImpl.wrapper(catalog),
        NativeInfoExtractorImpl.wrapper(schema),
        NativeInfoExtractorImpl.wrapper(tableName),
        types);
  }

  /**
   * 获取表结构
   *
   * @param catalog catalog
   * @param schema schema
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public Table getTableInfo(String catalog, String schema, String tableName, TableType... types) {
    return this.getTableInfo(catalog, schema, tableName, false, types);
  }

  /**
   * 表是否存在
   *
   * @param catalog catalog
   * @param schema schema
   * @param tableName 表名
   * @param types 表类型
   * @return 表结构信息
   */
  public boolean isTableExists(
      String catalog, String schema, String tableName, TableType... types) {
    return this.getTableInfo(catalog, schema, tableName, true, types) != null;
  }

  private Table getTableInfo(
      String catalog,
      String schema,
      String tableName,
      boolean checkTableExists,
      TableType... types) {
    NativeInfoExtractorImpl extractor = this.getExtractorService();
    TableInformation info = extractor.getTableInformation(catalog, schema, tableName, types);
    if (checkTableExists) {
      return info == null ? null : new Table();
    }

    if (info == null) {
      throw new RuntimeException(
          StrUtil.format(
              "Use Query(catalog = {}, schema = {}, tableName = {}) empty)",
              catalog,
              schema,
              tableName));
    }
    List<String> columnNames =
        extractor.getNativeTableColumns(catalog, schema, info.getName().getTableName().getText());

    this.metaConnectionClose(extractor);

    return Table.to(info, columnNames);
  }

  private ScriptSourceInput getScriptSourceInput(String[] sql) {
    StrBuilder sb = new StrBuilder();
    if (ArrayUtil.isNotEmpty(sql)) {
      for (String s : sql) {
        sb.append(s).append(System.lineSeparator());
      }
    }
    Reader reader = new StringReader(sb.toString());

    return new ScriptSourceInputFromReader(reader);
  }

  private ScriptTargetOutput getScriptTargetOutput() {
    return new ScriptTargetOutputToStdout();
  }

  private JpaTargetAndSourceDescriptor getJpaTargetAndSourceDescriptor(String[] sql) {
    return new JpaTargetAndSourceDescriptorImpl(
        SourceType.SCRIPT,
        this.getScriptSourceInput(sql),
        EnumSet.of(TargetType.DATABASE),
        this.getScriptTargetOutput());
  }

  /**
   * 创建表
   *
   * @param table {@link Table}
   */
  public void create(Table table) {
    Exportable exportable = new TableInfo(table, this);

    String[] sql =
        ExporterType.newInstance(ExporterType.TABLE, dialect)
            .getSqlCreateStrings(exportable, metadata, this.getSqlStringGenerationContext());

    JpaTargetAndSourceDescriptor descriptor = this.getJpaTargetAndSourceDescriptor(sql);

    this.creator.doCreation(metadata, this.getExecutionOptions(), descriptor, descriptor);
  }

  /**
   * 更新表<br>
   *
   * <ul>
   *   <li>不存在进行创建
   *   <li>存在进行结构更新
   * </ul>
   *
   * <p>需要跟踪Sql生成请移步{@link StandardTableExporter#getSqlCreateStrings(org.hibernate.mapping.Table,
   * Metadata, SqlStringGenerationContext)}
   *
   * @param table {@link Table}
   */
  @SuppressWarnings("unchecked")
  public void update(Table table) {
    TableInfo exportable = new TableInfo(table, this);
    NativeInfoExtractorImpl extractor = this.getExtractorService();
    TableInformation tableInfo =
        extractor.getTableInformation(
            this.getDefaultCatalog(), this.getDefaultSchema(), table.getName(), TableType.TABLE);
    ExecutionOptions options = this.getExecutionOptions();
    Map map = options.getConfigurationValues();
    map.put(SchemaUpdater.TABLE, exportable);
    map.put(SchemaUpdater.TABLE_INFO, tableInfo);

    this.updater.doMigration(
        metadata,
        options,
        new JpaTargetAndSourceDescriptorImpl(
            SourceType.METADATA, null, EnumSet.of(TargetType.DATABASE), null));

    this.metaConnectionClose(extractor);
  }

  /**
   * 删除表
   *
   * @param tableName {@link Table#getName()}
   */
  public void drop(String tableName) {
    this.drop(new Table(tableName));
  }

  /**
   * 删除表
   *
   * @param table {@link Table}
   */
  public void drop(Table table) {
    Exportable exportable = new TableInfo(table, this);
    String[] sql =
        ExporterType.newInstance(ExporterType.TABLE, dialect)
            .getSqlDropStrings(exportable, metadata, this.getSqlStringGenerationContext());

    JpaTargetAndSourceDescriptor descriptor = this.getJpaTargetAndSourceDescriptor(sql);

    this.dropper.doDrop(metadata, this.getExecutionOptions(), descriptor, descriptor);
  }

  /**
   * 先删除原来存在的创建新表
   *
   * @param table {@link Table}
   */
  public void createDrop(Table table) {
    this.drop(table.getName());
    this.create(table);
  }
}
